#!/usr/bin/env bash
set -euo pipefail
# set -x # Keep this commented out unless actively debugging

DEBUG=${SIGNOFF_DEBUG:-}
readonly VERSION="0.2.1"

# Status symbols for consistent display - export them for both script and tests
export STATUS_SUCCESS="✓"
export STATUS_PENDING="⟳"
export STATUS_FAILURE="✗"

trap 'fail "Unexpected error on line $LINENO ($?)"' ERR

if ! command -v git >/dev/null 2>&1; then
  echo "Error: git command not found" >&2
  exit 1
fi

if ! command -v gh >/dev/null 2>&1; then
  echo "Error: gh command not found. Please install GitHub CLI: https://cli.github.com" >&2
  exit 1
fi

# Cache for default branch
DEFAULT_BRANCH=""

debug() {
  if [[ -n "$DEBUG" ]]; then
    echo "Debug: $*" >&2
  fi
}

fail() {
  echo "Error: $*" >&2
  exit 1
}

# Get default branch with caching
default_branch() {
  if [[ -z "$DEFAULT_BRANCH" ]]; then
    DEFAULT_BRANCH=$(gh api repos/:owner/:repo --jq .default_branch 2>/dev/null) || return 1
  fi
  echo "$DEFAULT_BRANCH"
}

# Get signoff contexts from branch protection
get_signoff_contexts() {
  local branch="${1:-}"
  local contexts=()

  # If branch not specified, use default branch
  if [[ -z "$branch" ]]; then
    branch=$(default_branch) || return 0
  fi

  # Get branch protection
  local protection
  protection=$(gh api "repos/:owner/:repo/branches/${branch}/protection" 2>/dev/null) || return 0

  # Extract signoff contexts (strip 'signoff/' prefix for display)
  while read -r ctx; do
    [[ -z "$ctx" ]] && continue
    if [[ "$ctx" == "signoff/"* ]]; then
      contexts+=("${ctx#signoff/}")
    elif [[ "$ctx" == "signoff" ]]; then
      # Skip the default signoff context
      continue
    fi
  done < <(echo "$protection" | jq -r '.required_status_checks?.contexts? | map(select(startswith("signoff"))) | .[]?' 2>/dev/null || echo "")

  # Return contexts
  printf "%s\n" "${contexts[@]}"
}

is_clean() {
  local git_cmd=$(command -v git)

  if [[ -n "$($git_cmd status --porcelain)" ]]; then
    debug "found uncommitted changes"
    return 1
  fi

  if ! $git_cmd rev-parse --abbrev-ref @{push} >/dev/null 2>&1; then
    debug "no tracking branch found"
    fail "current branch is not tracking a remote branch"
  fi

  if [[ -n "$($git_cmd log @{push}..)" ]]; then
    debug "found unpushed changes"
    return 1
  fi

  return 0
}

cmd_create() {
  local force=false
  local contexts=()

  # Process arguments
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -f)
        force=true
        shift
        ;;

      -*)
        fail "unknown option: $1"
        ;;
      *)
        # Non-option argument is treated as context
        contexts+=("$1")
        shift
        ;;
    esac
  done

  if ! $force && ! is_clean; then
    fail "repository has uncommitted or unpushed changes"
  fi

  local user
  user=$(git config user.name) || fail "failed to get git user name"
  [[ -z "$user" ]] && fail "git user.name is not set"

  local sha
  sha=$(git rev-parse HEAD) || fail "failed to get current commit"

  # If no contexts specified, use default
  if [ ${#contexts[@]} -eq 0 ]; then
    contexts=("")
  fi

  local success=true
  local success_messages=()

  # Process each context
  for context in "${contexts[@]}"; do
    local context_name="signoff"
    if [[ -n "$context" ]]; then
      context_name="signoff/${context}"
    fi

    debug "creating status for commit ${sha} by ${user} with context ${context_name}"

    if gh api \
      --method POST \
      "repos/:owner/:repo/statuses/${sha}" \
      -f state=success \
      -f context="${context_name}" \
      -f "description=${user} signed off" >/dev/null; then

      # Build success message
      if [[ -z "$context" ]]; then
        success_messages+=("${STATUS_SUCCESS} Signed off on ${sha}")
      else
        success_messages+=("${STATUS_SUCCESS} Signed off on ${sha} for ${context}")
      fi
    else
      success=false
      if [[ -z "$context" ]]; then
        echo "${STATUS_FAILURE} Failed to sign off on ${sha}" >&2
      else
        echo "${STATUS_FAILURE} Failed to sign off on ${sha} for ${context}" >&2
      fi
    fi
  done

  # Show success messages if everything passed
  if $success; then
    printf "%s\n" "${success_messages[@]}"
  else
    exit 1
  fi
}

cmd_install() {
  local branch=""
  local contexts=()
  # All arguments except --branch are contexts

  # Process arguments with proper option handling
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --branch)
        if [[ -z "$2" || "$2" == -* ]]; then
          fail "option --branch requires an argument"
        fi
        branch="$2"
        shift 2
        ;;
      -*)
        fail "unknown option: $1"
        ;;
      *)
        # All positional arguments are contexts
        contexts+=("$1")
        shift
        ;;
    esac
  done

  # If branch not specified, use default branch
  if [[ -z "$branch" ]]; then
    branch=$(default_branch) ||
      fail "failed to get default branch"
  fi
  [[ -z "$branch" ]] && fail "branch name cannot be empty"

  # Default to standard signoff if no contexts specified
  if [ ${#contexts[@]} -eq 0 ]; then
    contexts=("")
  fi

  # Build API fields for all contexts
  local api_fields=()
  api_fields+=("--field" "required_status_checks[strict]=false")
  api_fields+=("--field" "enforce_admins=null")
  api_fields+=("--field" "required_pull_request_reviews=null")
  api_fields+=("--field" "restrictions=null")

  for context in "${contexts[@]}"; do
    local context_name="signoff"
    if [[ -n "$context" ]]; then
      context_name="signoff/${context}"
    fi
    api_fields+=("--field" "required_status_checks[contexts][]=${context_name}")
  done

  debug "installing protection on branch: ${branch} with ${#contexts[@]} contexts"

  gh api "/repos/:owner/:repo/branches/${branch}/protection" \
    --method PUT \
    -H "Accept: application/vnd.github+json" \
    -H "X-GitHub-Api-Version: 2022-11-28" \
    "${api_fields[@]}" >/dev/null || fail "failed to set branch protection"

  # Output success messages
  if [ ${#contexts[@]} -eq 1 ] && [ -z "${contexts[0]}" ]; then
    echo "${STATUS_SUCCESS} GitHub ${branch} branch now requires signoff"
  else
    for context in "${contexts[@]}"; do
      if [[ -z "$context" ]]; then
        echo "${STATUS_SUCCESS} GitHub ${branch} branch now requires signoff"
      else
        echo "${STATUS_SUCCESS} GitHub ${branch} branch now requires signoff on ${context}"
      fi
    done
  fi
}

cmd_uninstall() {
  local branch=""
  local contexts=()
  # All arguments except --branch are contexts

  # Process arguments with option handling
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --branch)
        if [[ -z "$2" || "$2" == -* ]]; then
          fail "option --branch requires an argument"
        fi
        branch="$2"
        shift 2
        ;;
      -*)
        fail "unknown option: $1"
        ;;
      *)
        # All positional arguments are contexts
        contexts+=("$1")
        shift
        ;;
    esac
  done

  if [[ -z "$branch" ]]; then
    branch=$(default_branch) || fail "failed to get default branch"
  fi
  [[ -z "$branch" ]] && fail "branch name cannot be empty"

  # TODO: Implement context-specific uninstall that preserves other contexts
  # For now we just remove all branch protection which is the current behavior
  debug "removing protection from branch: ${branch}"

  gh api \
    --method DELETE \
    "repos/:owner/:repo/branches/${branch}/protection" >/dev/null || fail "failed to remove branch protection"

  # Default to standard signoff if no contexts specified
  if [ ${#contexts[@]} -eq 0 ]; then
    contexts=("")
  fi

  # Output success messages
  if [ ${#contexts[@]} -eq 1 ] && [ -z "${contexts[0]}" ]; then
    echo "${STATUS_SUCCESS} GitHub ${branch} branch no longer requires signoff"
  else
    for context in "${contexts[@]}"; do
      if [[ -z "$context" ]]; then
        echo "${STATUS_SUCCESS} GitHub ${branch} branch no longer requires signoff"
      else
        echo "${STATUS_SUCCESS} GitHub ${branch} branch no longer requires signoff on ${context}"
      fi
    done
  fi
}

cmd_check() {
  local branch=""
  local contexts=()
  # All arguments except --branch are contexts

  # Process arguments with option handling
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --branch)
        if [[ -z "$2" || "$2" == -* ]]; then
          fail "option --branch requires an argument"
        fi
        branch="$2"
        shift 2
        ;;
      -*)
        fail "unknown option: $1"
        ;;
      *)
        # All positional arguments are contexts
        contexts+=("$1")
        shift
        ;;
    esac
  done

  if [[ -z "$branch" ]]; then
    branch=$(default_branch) || fail "failed to get default branch"
  fi
  [[ -z "$branch" ]] && fail "branch name cannot be empty"

  # Default to standard signoff if no contexts specified
  if [ ${#contexts[@]} -eq 0 ]; then
    contexts=("")
  fi

  # Fetch branch protection once
  local protection
  protection=$(gh api "repos/:owner/:repo/branches/${branch}/protection" 2>/dev/null) || {
    for context in "${contexts[@]}"; do
      if [[ -z "$context" ]]; then
        echo "${STATUS_FAILURE} GitHub ${branch} branch does not require signoff"
      else
        echo "${STATUS_FAILURE} GitHub ${branch} branch does not require signoff on ${context}"
      fi
    done
    return 1
  }

  # Check each context
  for context in "${contexts[@]}"; do
    local context_name="signoff"
    if [[ -n "$context" ]]; then
      context_name="signoff/${context}"
    fi

    debug "checking protection for branch: ${branch} with context: ${context_name}"

    if echo "$protection" | jq -e ".required_status_checks.contexts | contains([\"${context_name}\"])" >/dev/null 2>&1; then
      if [[ -z "$context" ]]; then
        echo "${STATUS_SUCCESS} GitHub ${branch} branch requires signoff"
      else
        echo "${STATUS_SUCCESS} GitHub ${branch} branch requires signoff on ${context}"
      fi
    else
      if [[ -z "$context" ]]; then
        echo "${STATUS_FAILURE} GitHub ${branch} branch does not require signoff"
      else
        echo "${STATUS_FAILURE} GitHub ${branch} branch does not require signoff on ${context}"
      fi
    fi
  done
}

cmd_status() {
  local branch=""
  local sha=""

  # Process arguments with option handling
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --branch)
        if [[ -z "$2" || "$2" == -* ]]; then
          fail "option --branch requires an argument"
        fi
        branch="$2"
        shift 2
        ;;
      -*)
        fail "unknown option: $1"
        ;;
      *)
        # For now we don't support filtering by context
        fail "unexpected argument: $1"
        ;;
    esac
  done

  # If branch not specified, use default branch
  if [[ -z "$branch" ]]; then
    branch=$(default_branch) || fail "failed to get default branch"
  fi
  [[ -z "$branch" ]] && fail "branch name cannot be empty"

  # Get current SHA
  sha=$(git rev-parse HEAD) || fail "failed to get current commit"

  # Get commit statuses
  local statuses
  statuses=$(gh api "repos/:owner/:repo/commits/${sha}/status" 2>/dev/null) || {
    echo "${STATUS_FAILURE} Could not get status for commit ${sha}"
    return 1
  }

  # Create required contexts list - always start with "signoff"
  local required=("signoff")

  # Add any additional contexts from branch protection
  local protection
  protection=$(gh api "repos/:owner/:repo/branches/${branch}/protection" 2>/dev/null) || debug "No branch protection found"
  if [[ -n "$protection" ]]; then
    # Add required signoff contexts from branch protection
    while read -r ctx; do
      [[ -z "$ctx" || "$ctx" == "signoff" ]] && continue  # Skip empty or default signoff (already included)
      required+=("$ctx")
    done < <(echo "$protection" | jq -r '.required_status_checks?.contexts? | map(select(startswith("signoff"))) | .[]?' 2>/dev/null || echo "")
  fi

  # Extract context/state pairs into a string-based lookup map
  # Format: "context1=state1;context2=state2;..."
  # This is a lightweight alternative to associative arrays for Bash 3
  local context_map=""

  # Extract all contexts with success state
  while IFS=$'\t' read -r context state; do
    [[ -z "$context" ]] && continue
    # Add to our string-based map
    context_map="${context_map}${context}=${state};"
  done < <(echo "$statuses" | jq -r '.statuses[]? | select(.context? | startswith("signoff")) | [.context, .state] | @tsv' 2>/dev/null || echo "")

  # Add any actual contexts that aren't in required list
  while read -r context; do
    [[ -z "$context" ]] && continue

    # Check if this context is already in required list
    local found=false
    for req in "${required[@]}"; do
      if [[ "$req" == "$context" ]]; then
        found=true
        break
      fi
    done

    # If not in required list, add it
    if [[ "$found" == "false" ]]; then
      required+=("$context")
    fi
  done < <(echo "$statuses" | jq -r '.statuses[]? | select(.context? | startswith("signoff")) | .context' 2>/dev/null || echo "")

  # Generate status output for all contexts (required + actual)
  local all_results=()
  for context in "${required[@]}"; do
    local display_name="$context"
    [[ "$context" == "signoff/"* ]] && display_name="${context#signoff/}"
    [[ "$context" == "signoff" ]] && display_name="signoff"

    # Check if we have a successful status for this context
    # by looking for "context=success;" in our map
    if [[ "$context_map" == *"${context}=success;"* ]]; then
      all_results+=("${STATUS_SUCCESS} $display_name")
    else
      all_results+=("${STATUS_FAILURE} $display_name")
    fi
  done

  # Print all results
  printf "%s\n" "${all_results[@]}"
}

cmd_completion() {
  # Check if --contexts flag is passed
  if [[ "${1:-}" == "--contexts" ]]; then
    get_signoff_contexts
    return
  fi

  cat <<'EOF'
# bash completion for gh signoff

_gh_signoff_contexts() {
  # Get dynamic contexts from branch protection
  contexts=$(gh signoff completion --contexts 2>/dev/null)
  echo "$contexts"
}

_gh_signoff() {
  local cur prev
  cur="${COMP_WORDS[COMP_CWORD]}"
  prev="${COMP_WORDS[COMP_CWORD-1]}"

  case "$prev" in
    signoff)
      # Include dynamic contexts in top-level completion
      local contexts=$(_gh_signoff_contexts)
      COMPREPLY=( $(compgen -W "create install uninstall check status version -f --help $contexts" -- "$cur") )
      return 0
      ;;
    create|install|uninstall|check)
      local contexts=$(_gh_signoff_contexts)
      COMPREPLY=( $(compgen -W "--branch $contexts" -- "$cur") )
      return 0
      ;;
    status)
      COMPREPLY=( $(compgen -W "--branch" -- "$cur") )
      return 0
      ;;
  esac

  # If we are halfway through typing an option
  if [[ $cur == --* ]]; then
    COMPREPLY=( $(compgen -W "--branch" -- "$cur") )
    return 0
  fi

  # For partial signoffs, suggest context names
  if [[ $COMP_CWORD -gt 1 && "${COMP_WORDS[1]}" != "install" &&
        "${COMP_WORDS[1]}" != "uninstall" && "${COMP_WORDS[1]}" != "check" &&
        "${COMP_WORDS[1]}" != "status" && "${COMP_WORDS[1]}" != "version" &&
        "${COMP_WORDS[1]}" != "completion" && "${COMP_WORDS[1]}" != "--help" &&
        "${COMP_WORDS[1]}" != "-h" ]]; then
    local contexts=$(_gh_signoff_contexts)
    COMPREPLY=( $(compgen -W "$contexts" -- "$cur") )
    return 0
  fi

  return 0
}

complete -F _gh_signoff gh-signoff
EOF
}

cmd_version() {
  echo "gh-signoff ${VERSION}"
}

cmd_help() {
  cat <<'EOF'
Sign off on commits without CI infrastructure.

USAGE
  gh signoff [flags] [command] [options]

COMMANDS
  create (default) Sign off on the current commit
  install          Install signoff requirement
  uninstall        Uninstall signoff requirement
  check            Check if signoff is required
  status           Show signoff status for the current commit
  version          Show gh-signoff version
  completion       Output shell completion code

FLAGS
  -f  Force sign off (ignore uncommitted/unpushed changes)

OPTIONS
  --branch <branch>  Branch to operate on (for install, uninstall, check)

EXAMPLES
  gh signoff                         # Sign off on current commit
  gh signoff create -f               # Force signoff
  gh signoff install                 # Require signoff on default branch
  gh signoff install --branch other  # Require signoff on a specific branch
  gh signoff check                   # Check if signoff is required
  gh signoff check --branch other    # Check if signoff is required on branch
  gh signoff status                  # Show completed/pending signoff status

COMPLETION
  # Add to ~/.bashrc:
  eval "$(gh signoff completion)"
EOF
}

case "${1:-}" in
  "")               cmd_create "$@" ;;
  "create")         shift; cmd_create "$@" ;;
  "install")        shift; cmd_install "$@" ;;
  "uninstall")      shift; cmd_uninstall "$@" ;;
  "check")          shift; cmd_check "$@" ;;
  "status")         shift; cmd_status "$@" ;;
  "version")        cmd_version ;;
  "completion")     shift; cmd_completion "$@" ;;
  "-h" | "--help")  cmd_help ;;
  *)
    # Handle partial signoff contexts directly
    if [[ "$1" == "-f" ]]; then
      # Special case - forced signoff with no context
      cmd_create -f
    elif [[ "$1" == -* ]]; then
      # Unknown option
      cmd_help; exit 1
    else
      # Treat as partial signoff context(s)
      cmd_create "$@"
    fi
    ;;
esac
